﻿using CloudStorageLight.Core;
using Google.Apis.Auth.OAuth2;
using Google.Apis.Auth.OAuth2.Flows;
using Google.Apis.Auth.OAuth2.Mvc;
using Google.Apis.Auth.OAuth2.Requests;
using Google.Apis.Auth.OAuth2.Responses;
using Google.Apis.Auth.OAuth2.Web;
using Google.Apis.Drive.v2;
using Google.Apis.Drive.v2.Data;
using Google.Apis.Services;
using Google.Apis.Util.Store;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Mvc;

namespace CloudStorageLight.Core
{
    [ExcludeFromCodeCoverage]
    public class GoogleDriveBlobAdapter : BlobAdapterBase
    {
        string clientId;
        string clientSecret;
        string refreshToken;
        internal string googleFilePath;

        private DriveService googleDriveService;


        public GoogleDriveBlobAdapter(string rootPath, string accessToken, string refreshToken, string googleFilePath, string timezone = null) 
            : this(rootPath, SystemSettings.Instance.GoogleDriveClientId, SystemSettings.Instance.GoogleDriveClientSecret, refreshToken, googleFilePath, timezone)
        {

        }

        public GoogleDriveBlobAdapter(string rootPath, string clientId, string clientSecret, string refreshToken, string googleFilePath, string timezone)
        {
            this.RootPath = rootPath;
            this.clientId = clientId;
            this.clientSecret = clientSecret;
            this.refreshToken = refreshToken;
            this.googleFilePath = googleFilePath.TrimEnd('/') + "/";
            this.TimeZone = timezone;

        }

        public string ConvertGooglePathToUrlPath(string dropPath)
        {
            if (googleFilePath.Length > dropPath.Length) return RootPath;
            var uriPath = dropPath.Substring(googleFilePath.Length);
            uriPath = RootPath + uriPath;
            return uriPath;
        }

        public string ConvertUrlPathToGooglePath(string urlPath)
        {
            return this.googleFilePath + urlPath.Substring(RootPath.Length);
        }

        private DriveService GetGoogleDriveService()
        {
            if (googleDriveService == null)
            {
                var data = new DictionaryDataStore();
                data.Data[this.RootPath] = new TokenResponse()
                {
                    RefreshToken = this.refreshToken,
                    TokenType = "Bearer",
                    Issued = DateTime.MinValue
                };
                var flow = new AppGoogleAuthorizationCodeFlow(new GoogleAuthorizationCodeFlow.Initializer
                {
                    ClientSecrets = new ClientSecrets
                    {
                        ClientId = this.clientId,
                        ClientSecret = this.clientSecret,

                    },
                    Scopes = new[] { DriveService.Scope.Drive },
                    DataStore = data
                });

                var auth = new AppAuthorizationCodeWebApp(
                    this.RootPath,
                    new AppFlowMetadata(this.RootPath, this.clientId, this.clientSecret, null, flow),
                    null, null);
                var result = auth.AuthorizeAsync(CancellationToken.None).Result;

                this.googleDriveService = new DriveService(new BaseClientService.Initializer
                {
                    HttpClientInitializer = result.Credential,
                    ApplicationName = "Cloud Storage Light"
                });
            }

            return this.googleDriveService;
        }

        async Task<Google.Apis.Drive.v2.Data.File> GetFileByUrlPathAsync(DriveService client, string path)
        {
            return await GetFileAsync(client, ConvertUrlPathToGooglePath(path));
        }


        async Task<Google.Apis.Drive.v2.Data.File> GetFileAsync(DriveService client, string path)
        {
            if (fileCahce.ContainsKey(path)) return fileCahce[path];
            if (path == "/")
            {
                var f = await client.Files.Get("root").ExecuteAsync();
                fileCahce["/"] = f;
                return f;
            }
            var parent = GetParentFolder(path);
            if (parent == null) return null;
            var parentFolder = await GetFileAsync(client, parent);
            if (parentFolder == null) return null;
            var name = path.Substring(parent.Length).TrimEnd('/');

            var query = client.Files.List();
            query.Q = "'" + parentFolder.Id + "' in parents and trashed=false and Title='" + name +"'";
            var list = await query.ExecuteAsync();
            var item = list.Items.Where(x=>x.ModifiedDate.HasValue).OrderByDescending(x => x.ModifiedDate).FirstOrDefault();
            if (item != null)
            {
                fileCahce[parent + item.Title] = item;
            }
            return item;
        }

        /// <summary>
        /// 指定したファイルが存在するか
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        protected override async Task<bool> ExistsAsyncInternal(string path)
        {
            return (await GetFileByUrlPathAsync(GetGoogleDriveService(), path)) != null;

        }
        private Dictionary<string, Google.Apis.Drive.v2.Data.File> fileCahce = new Dictionary<string, Google.Apis.Drive.v2.Data.File>();



        string GetParentFolder(string path)
        {
            path = path.TrimEnd('/');
            return path.Substring(0, path.Length - path.Split('/').Last().Length);
        }

        string GetFileName(string path)
        {
            return path.Substring(path.Length - path.Split('/').Last().Length);
        }

        /// <summary>
        /// Blobのコピー
        /// </summary>
        /// <param name="path"></param>
        /// <param name="fromUri"></param>
        /// <returns></returns>
        public override async Task CopyAsync(string path, BlobFile file)
        {

            var client = GetGoogleDriveService();
            var folderDest = await GetFileByUrlPathAsync(client, GetParentFolder(path));
            var fileSrc = await GetFileByUrlPathAsync(client, file.Path);
            if (folderDest == null) folderDest = await CreateDirectoryAsync(client, ConvertUrlPathToGooglePath(GetParentFolder(path)));
            var fileDest = new Google.Apis.Drive.v2.Data.File();
            fileDest.Title = GetFileName(path);
            fileDest.MimeType = fileSrc.MimeType;
            fileDest.Parents = new List<Google.Apis.Drive.v2.Data.ParentReference>() { new ParentReference() { Id = folderDest.Id } };
            await client.Files.Copy(fileDest, fileSrc.Id).ExecuteAsync();
        }

        private async Task<Google.Apis.Drive.v2.Data.File> CreateDirectoryAsync(DriveService client, string folderPath)
        {
            var folder = await GetFileAsync(client, folderPath);
            if (folder != null) return folder;
            var parentPath = GetParentFolder(folderPath);
            folder = await CreateDirectoryAsync(client, parentPath);

            var newFolder = new Google.Apis.Drive.v2.Data.File();
            newFolder.Title = GetFileName(folderPath.TrimEnd('/'));
            newFolder.MimeType = "application/vnd.google-apps.folder";            
            newFolder.Parents = new List<ParentReference>() { new ParentReference() { Id = folder.Id } };

            return await client.Files.Insert(newFolder).ExecuteAsync();
        }
        /// <summary>
        /// 削除
        /// </summary>
        /// <returns></returns>
        public override async Task<bool> DeleteAsync(BlobItem file)
        {
            var client = GetGoogleDriveService();
            var driveFile = await GetFileByUrlPathAsync(client, file.Path);
            if (driveFile == null) return false;
            await client.Files.Trash(driveFile.Id).ExecuteAsync();
            ClearExistsCahce();
            return true;

        }

        /// <summary>
        /// ディレクトリののファイル一覧を取得する
        /// </summary>
        /// <param name="id"></param>
        /// <param name="maxSize"></param>
        /// <returns></returns>
        public override async Task<List<BlobItem>> GetFilesAsync(string id, int maxSize, bool flat = false)
        {
            var result = new List<BlobItem>();
            var client = GetGoogleDriveService();
            var logicalPath = id.StartsWith(RootPath) ? id : RootPath;
            await ListFilesAsync(logicalPath, maxSize, client, result, flat);
            return result;

        }


        private bool IsValidFileNmae(string filename)
        {
            return !Path.GetInvalidFileNameChars().Any(x => filename.Contains(x));
        }
        private async Task ListFilesAsync(string urlPath, int maxSize, DriveService client, List<BlobItem> result, bool flat)
        {
            var folders = new List<BlobDirectory>();
            var driveFile = await GetFileByUrlPathAsync(client, urlPath);
            var query = client.Files.List();
            query.Q = "'" + driveFile.Id + "' in parents and trashed=false";


            do
            {
                var list = await query.ExecuteAsync();

                foreach (var item in list.Items)
                {
                    if (!IsValidFileNmae(item.Title)) continue;
                    BlobItem fileItem = null;
                    string logicalPath;
                    if (item.MimeType == "application/vnd.google-apps.folder")
                    {
                        logicalPath = urlPath + item.Title + "/";
                        var folder = new BlobDirectory(Parent, logicalPath);
                        folders.Add(folder);
                        fileItem = folder;
                    }
                    else
                    {
                        logicalPath = urlPath + item.Title;
                        fileItem = new BlobFile(Parent, logicalPath);
                        fileItem.Length = item.FileSize ?? 0;
                        fileItem.LastModified = ConvertTimezone(item.ModifiedDate);
                        fileItem.Owner = item.LastModifyingUserName;
                    }
                    var drivePath = ConvertUrlPathToGooglePath(logicalPath);

                    if (!fileCahce.ContainsKey(drivePath) 
                        || (fileCahce[drivePath].ModifiedDate ?? DateTime.MinValue) < (item.ModifiedDate ?? DateTime.MinValue))
                    {
                        fileCahce[drivePath] = item;
                        if (fileItem != null) result.Add(fileItem);
                    }

                    if (maxSize > 0 && result.Count >= maxSize)
                    {
                        query.PageToken = null;
                        break;
                    }
                }
                query.PageToken = list.NextPageToken;
            } while (!String.IsNullOrEmpty(query.PageToken));

            if (flat)
            {
                foreach (var folder in folders)
                {
                    await ListFilesAsync(folder.Path, maxSize, client, result, flat);
                    if (maxSize > 0 && result.Count >= maxSize)
                    {
                        break;
                    }
                }
            }
        }

        private static string GetMimeType(string fileName)
        {
            string mimeType = "application/unknown";
            string ext = System.IO.Path.GetExtension(fileName).ToLower();
            Microsoft.Win32.RegistryKey regKey = Microsoft.Win32.Registry.ClassesRoot.OpenSubKey(ext);
            if (regKey != null && regKey.GetValue("Content Type") != null)
                mimeType = regKey.GetValue("Content Type").ToString();
            return mimeType;
        }

        /// <summary>
        /// アップロード
        /// </summary>
        /// <param name="stream"></param>
        /// <returns></returns>
        public override async Task UploadAsync(BlobItem item, Stream stream)
        {
            var client = GetGoogleDriveService();
            var path = GetParentFolder(item.Path);
            var folder = await GetFileByUrlPathAsync(client, path);

            var newFile = new Google.Apis.Drive.v2.Data.File();
            newFile.Title = GetFileName(item.Path);
            newFile.MimeType = GetMimeType(GetFileName(item.Path));
            newFile.Parents = new List<ParentReference>() { new ParentReference() { Id = folder.Id } };

            var driveFile = await GetFileByUrlPathAsync(client, item.Path);
            if (driveFile == null)
            {
                var request = client.Files.Insert(newFile, stream, GetMimeType(GetFileName(item.Path)));
                var result = await request.UploadAsync();
                if (result == null) throw new BlobException(Messages.MsgGeneral);
            }
            else
            {
                var request = client.Files.Update(newFile, driveFile.Id, stream, GetMimeType(GetFileName(item.Path)));
                var result = await request.UploadAsync();
                if (result == null) throw new BlobException(Messages.MsgGeneral);                
            }
            
            ClearExistsCahce();
        }

        /// <summary>
        /// アップロード
        /// </summary>
        /// <param name="localpath"></param>
        /// <returns></returns>
        public override async Task UploadAsync(BlobItem item, string localpath)
        {
            using (var st = new FileStream(localpath, FileMode.Open))
            {
                await UploadAsync(item, st);
            }
        }

        /// <summary>
        /// ダウンロード
        /// </summary>
        /// <param name="stream"></param>
        /// <returns></returns>
        public override async Task DownloadAsync(BlobItem item, Stream stream)
        {
            var client = GetGoogleDriveService();
            var driveFile = await GetFileByUrlPathAsync(client, item.Path);

            using (HttpResponseMessage response = await client.HttpClient.GetAsync(driveFile.DownloadUrl))
            using (Stream streamToReadFrom = await response.Content.ReadAsStreamAsync())
            {
                await streamToReadFrom.CopyToAsync(stream);
            }
        }

        /// <summary>
        /// ダウンロード
        /// </summary>
        /// <param name="localpath"></param>
        /// <returns></returns>
        public override async Task DownloadAsync(BlobItem item, string localpath)
        {
            using (var st = new FileStream(localpath, FileMode.Create))
            {
                await DownloadAsync(item, st);
            }
        }

        /// <summary>
        /// フォルダ作成
        /// </summary>
        /// <param name="dire"></param>
        /// <returns></returns>
        public override async Task CreateDirectoryAsync(BlobDirectory dire)
        {
            var client = GetGoogleDriveService();
            await CreateDirectoryAsync(client, ConvertUrlPathToGooglePath(dire.Path));
            ClearExistsCahce();
        }

        /// <summary>
        /// メタ情報の設定
        /// </summary>
        /// <returns></returns>
        #pragma warning disable 1998
        public override async Task SetMetadataAsync(BlobItem item)
        {
            item.Link = ConvertUrlPathToGooglePath(item.Path);

        }
        #pragma warning restore 1998

        /// <summary>
        /// メタ情報の取得
        /// </summary>
        /// <param name="fetch"></param>
        /// <returns></returns>
        public override async Task GetMetadataAsync(BlobItem file)
        {
            var client = GetGoogleDriveService();
            var driveFile = await GetFileByUrlPathAsync(client, file.Path);
            if (driveFile != null) file.LastModified = ConvertTimezone(driveFile.ModifiedDate);
        }

        public override void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }


        bool disposed = false;
        protected virtual void Dispose(bool disposing)
        {
            if (disposed) return;
            if (disposing)
            {
                if (googleDriveService != null) this.googleDriveService.Dispose();
                googleDriveService = null;
            }

            disposed = true;
        }
    }



}
